#!python
# Copyright 2012 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Presubmit script for Syzygy.

import itertools
import os
import re
import sys


# Determine the root of the source tree. We use getcwd() instead of __file__
# because gcl loads this script as text and runs it using eval(). In this
# context the variable __file__ is undefined. However, gcl assures us that
# the current working directory will be the directory containing this file.
SYZYGY_ROOT_DIR = os.path.abspath(os.getcwd())


# Bring in some presubmit tools.
sys.path.insert(0, os.path.join(SYZYGY_ROOT_DIR, 'py'))
import test_utils.presubmit as presubmit  # pylint: disable=F0401


# Bring in internal-only presubmit checks. These live in a parallel
# repository that is overlaid with the public version of syzygy. The
# internal presubmit check is expected to live in the 'internal'
# subdirectory off the syzygy root.
try:
  internal_dir = os.path.join(SYZYGY_ROOT_DIR, 'internal')
  if os.path.isdir(internal_dir):
    sys.path.insert(0, internal_dir)
  import internal_presubmit  # pylint: disable=F0401
except ImportError:
  internal_presubmit = None


_UNITTEST_MESSAGE = """\
Your %%s unittests must succeed before submitting! To clear this error,
  run: %s""" % os.path.join(SYZYGY_ROOT_DIR, 'run_all_tests.bat')


# License header and copyright line taken from:
# http://go/ossreleasing#Apache_License
_LICENSE_HEADER = """\
(#!python\n\
)?.*? Copyright 20[0-9][0-9] Google Inc\. All Rights Reserved\.\n\
.*?\n\
.*? Licensed under the Apache License, Version 2\.0 \(the "License"\);\n\
.*? you may not use this file except in compliance with the License\.\n\
.*? You may obtain a copy of the License at\n\
.*?\n\
.*?     http://www\.apache\.org/licenses/LICENSE-2\.0\n\
.*?
.*? Unless required by applicable law or agreed to in writing, software\n\
.*? distributed under the License is distributed on an "AS IS" BASIS,\n\
.*? WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.\n\
.*? See the License for the specific language governing permissions and\n\
.*? limitations under the License\.\n\
"""


# Regular expressions to recognize source and header files.
# These are lifted from presubmit_support.py in depot_tools and are
# formulated as a list of regex strings so that they can be passed to
# input_api.FilterSourceFile() as the white_list parameter.
_CC_SOURCES = (r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.rc$')
_CC_HEADERS = (r'.+\.h$', r'.+\.inl$', r'.+\.hxx$', r'.+\.hpp$')
_CC_FILES = _CC_SOURCES + _CC_HEADERS
_CC_SOURCES_RE = re.compile('|'.join('(?:%s)' % x for x in _CC_SOURCES))


# Regular expressions used to extract headers and recognize empty lines.
_INCLUDE_RE = re.compile(r'^\s*#\s*include\s+(?P<header>[<"][^<"]+[>"])'
                         r'\s*(?://.*(?P<nolint>NOLINT).*)?$')
_COMMENT_OR_BLANK_RE = re.compile(r'^\s*(?://.)?$')


def _IsSourceHeaderPair(source_path, header):
  # Returns true if source and header are a matched pair.
  # Source is the path on disk to the source file and header is the include
  # reference to the header (i.e., "blah/foo.h" or <blah/foo.h> including the
  # outer quotes or brackets.
  if not _CC_SOURCES_RE.match(source_path):
    return False

  source_root = os.path.splitext(source_path)[0]
  if source_root.endswith('_unittest'):
    source_root = source_root[0:-9]
  include = os.path.normpath(source_root + '.h')
  header = os.path.normpath(header[1:-1])

  return include.endswith(header)


def _GetHeaderCompareKey(source_path, header):
  if _IsSourceHeaderPair(source_path, header):
    # We put the header that corresponds to this source file first.
    group = 0
  elif header.startswith('<'):
    # C++ system headers should come after C system headers.
    group = 1 if header.endswith('.h>') else 2
  else:
    group = 3
  dirname, basename = os.path.split(header[1:-1])
  return (group, dirname, basename)


def _GetHeaderCompareKeyFunc(source):
  return lambda header : _GetHeaderCompareKey(source, header)


def _HeaderGroups(source_lines):
  # Generates lists of headers in source, one per block of headers.
  # Each generated value is a tuple (line, headers) denoting on which
  # line of the source file an uninterrupted sequences of includes begins,
  # and the list of included headers (paths including the quotes or angle
  # brackets).
  start_line, headers = None, []
  for line, num in itertools.izip(source_lines, itertools.count(1)):
    match = _INCLUDE_RE.match(line)
    if match:
      # The win32 api has all sorts of implicit include order dependencies.
      # Rather than encode exceptions for these, we require that they be
      # excluded from the ordering by a // NOLINT comment.
      if not match.group('nolint'):
        headers.append(match.group('header'))
      if start_line is None:
        # We just started a new run of headers.
        start_line = num
    elif headers and not _COMMENT_OR_BLANK_RE.match(line):
      # Any non-empty or non-comment line interrupts a sequence of includes.
      assert start_line is not None
      yield (start_line, headers)
      start_line = None
      headers = []

  # Just in case we have some headers we haven't yielded yet, this is our
  # last chance to do so.
  if headers:
    assert start_line is not None
    yield (start_line, headers)


def CheckIncludeOrder(input_api, output_api):
  """Checks that the C/C++ include order is correct."""
  errors = []
  is_cc_file = lambda x: input_api.FilterSourceFile(x, white_list=_CC_FILES)
  for f in input_api.AffectedFiles(include_deletes=False,
                                   file_filter=is_cc_file):
    for line_num, group in _HeaderGroups(f.NewContents()):
      sorted_group = sorted(group, key=_GetHeaderCompareKeyFunc(f.LocalPath()))
      if group != sorted_group:
        message = '%s, line %s: Out of order includes. ' \
                  'Expected:\n\t#include %s' % (
                      f.LocalPath(),
                      line_num,
                      '\n\t#include '.join(sorted_group))
        errors.append(output_api.PresubmitPromptWarning(message))
  return errors


def CheckUnittestsRan(input_api, output_api, committing, configuration):
  """Checks that the unittests success file is newer than any modified file"""
  return presubmit.CheckTestSuccess(input_api, output_api, committing,
                                    configuration, 'ALL',
                                    message=_UNITTEST_MESSAGE % configuration)


def CheckChange(input_api, output_api, committing):
  # The list of (canned) checks we perform on all files in all changes.
  checks = [
    CheckIncludeOrder,
    input_api.canned_checks.CheckChangeHasDescription,
    input_api.canned_checks.CheckChangeHasNoCrAndHasOnlyOneEol,
    input_api.canned_checks.CheckChangeHasNoTabs,
    input_api.canned_checks.CheckChangeHasNoStrayWhitespace,
    input_api.canned_checks.CheckLongLines,
    input_api.canned_checks.CheckChangeSvnEolStyle,
    input_api.canned_checks.CheckDoNotSubmit,
  ]

  results = []
  for check in checks:
    results += check(input_api, output_api)

  # We run lint only on C/C++ files so that we avoid getting notices about
  # files being ignored.
  is_cc_file = lambda x: input_api.FilterSourceFile(x, white_list=_CC_FILES)
  results += input_api.canned_checks.CheckChangeLintsClean(
      input_api, output_api, source_file_filter=is_cc_file)

  # We check the license on the default recognized source file types, as well
  # as GYP and Python files.
  gyp_file_re = r'.+\.gypi?$'
  py_file_re = r'.+\.py$'
  white_list = input_api.DEFAULT_WHITE_LIST + (gyp_file_re, py_file_re)
  sources = lambda x: input_api.FilterSourceFile(x, white_list=white_list)
  results += input_api.canned_checks.CheckLicense(input_api,
                                                  output_api,
                                                  _LICENSE_HEADER,
                                                  source_file_filter=sources)

  results += CheckUnittestsRan(input_api, output_api, committing, "Debug")
  results += CheckUnittestsRan(input_api, output_api, committing, "Release")

  if internal_presubmit:
    results += internal_presubmit.CheckChange(input_api,
                                              output_api,
                                              committing)

  return results


def CheckChangeOnUpload(input_api, output_api):
  return CheckChange(input_api, output_api, False)


def CheckChangeOnCommit(input_api, output_api):
  return CheckChange(input_api, output_api, True)
